OCaml 4.02

48
7
sept.
2014
Programmation fonctionnelle

La version 4.02 du langage OCaml a été annoncée vendredi 29 août. Elle fait suite à la version 4.01 publiée en septembre 2013. Il s'agit de l'implémentation la plus vivace du langage ML (Meta language) et l'un des langages fonctionnels les plus utilisés (comme Haskell, XSL, Lisp…)

Sommaire

Ce changement mineur de version n'est pas représentatif des évolutions présentes. Cette version inclut des changements dans la syntaxe du langage et des outils, pouvant casser la compatibilité avec les programmes existants.

Les changements d'outils et de bibliothèque

Points d'extensions et séparation de Camlp4

L'une des évolutions les plus attendues modifie le paradigme de méta-programmation d'OCaml en introduisant une étape de compilation permettant de transformer l'arbre syntaxique du programme, un mécanisme analogue¹ aux macros Lisps.

¹ Mais seulement analogue !

Le paradigme de méta-programmation précédent s'appuyait sur l'historique pré-processeur camlp4 qui permettait de définir des dialectes d'OCaml en enrichissant sa syntaxe. Les applications principales de ce type de pré-processeur sont :

  • La représentation de données externes dans le système de types du programme ce qui permet de garantir à la compilation que l'ensemble des types sont cohérents entre eux. La bibliothèque PG'OCaml permet ainsi de vérifier que la cohérence de type des requêtes SQL avec le code OCaml à la compilation.

  • La génération automatique de fonctions de sérialisation et déserialisation de structures de données, ce qu'implémente la sexplib.

  • La création de sous-langages propres à un langage dédié, ceci est utilisé par le serveur web OCsigen de sorte qu'un document HTML généré par un programme OCaml/OCsigen valide est valide.

Les principaux défauts de camlp4 sont sa complexité et l'augmentation du temps de compilation que son utilisation implique : un programme traité par camlp4 passe une fois dans l'analyseur syntaxique de camlp4 puis le texte correspondant passe dans l'analyseur syntaxique du compilateur ocaml. De plus, camlp4 faisant partie de la distribution d'ocaml les mises à jour ne pouvaient se faire que de pair avec celles du compilateur. Ce type de dépendance ralentit la correction et cause des bogues et des incompatibilités, parfois subtiles, avec la syntaxe d'OCaml. Si le dernier point devrait s'améliorer grâce à la séparation de ocamlp4 du compilateur, les autres restent d'actualité.

Le nouveau paradigme de méta-programmation faisant son arrivée dans OCaml est les points d'extensions, qui sont des éléments syntaxiques dont la forme est définie par le langage mais dont la sémantique est définie par un pré-processeur.

Souvenons-nous que lors de la compilation d'un programme, les instructions sont transformées dans un arbre syntaxique abstrait que le compilateur traduit en code machine — après d'éventuelles optimisations. Comme le langage OCaml se compile lui-même, la structure de cet arbre est une structure de donnée OCaml. Il est maintenant possible de modifier la structure de cet arbre à travers les fameux points d'extensions. L'intérêt premier est de travailler sur un objet déjà traité et construit par le compilateur, ce qui limite les erreurs et facilite l'écriture des extensions. On y gagne aussi en vitesse d'exécution puisque l'étape d'extension se fait dans le processus de compilation, et non pas via un appel externe. Enfin, puisque les points d'extensions ne modifient pas la syntaxe d'OCaml, faire cohabiter différentes extensions devrait être plus facile.

Aujourd'hui camlp4 est encore très largement utilisé. Le sortir du cœur d'OCaml ne va pas le faire disparaître immédiatement, car de nombreux modules n'ont pas encore leur équivalent via les points d'extensions. On peut toutefois maintenant supposer qu'il va mourir de sa belle mort, après avoir été utilisé pendant de nombreuses années et avoir été une figure majeure de l'univers OCaml. Le projet est désormais hébergé sur github, ce qui marque sa séparation d'avec le cœur d'OCaml.

(Par contre les amateurs de Lisp seront déçus : cet accès au code est limité au temps de la compilation, et le programme compilé n'a pas la possibilité de se modifier lui-même. Cela n'est possible qu'à travers un programme tiers.)

Séparation de Labltk

La bibliothèque Labltk était jusqu'à présent une partie intégrante de la distribution OCaml. Elle sera désormais disponible comme une bibliothèque tierce.

Nouvelles extensions au langage

Exceptions et filtrage par motif

La gestion des exceptions est très performante en OCaml. En compilant le programme avec les bonnes options, une exception est transformée en une simple instruction de saut, et est souvent utilisée pour sortir d'un traitement (boucle, etc). Cet usage des exceptions est maintenant mieux intégré dans le langage puisqu'une exception peut être traitée comme une valeur de retour et être intégrée dans un filtrage par motif.

En effet, l'utilisation d'exceptions dans du code récursif pouvait poser problème. Voici un petit exemple qui illustre la situation : le programme suivant permet de lire un fichier ligne par ligne et stocker l'ensemble des lignes dans une liste :

let read_lines inc =
  let rec loop acc = (* Schéma classique de récursion terminale à travers un accumulateur *)
    try
      let l = input_line inc in
      loop (l :: acc) (* Appel récursif en ajoutant la ligne lue à l'accumulateur *)
    with End_of_file -> List.rev acc (* Renvoie la liste accumulée quand on atteint la fin du fichier *)
  in
  loop []

Sauf que l'appel à la fonction loop est présent dans un gestionnaire d'exception, ce qui empêche la fonction d'être optimisée en récursion terminale. Sur un fichier de taille suffisamment importante, le programme va saturer la pile. Il est désormais possible d'utiliser la syntaxe suivante, qui considère que l'exception est une valeur de retour possible de la fonction :

let read_lines inc =
  let rec loop acc =
    match input_line inc with
      | l -> loop (l :: acc) (* Appel récursif en ajoutant la ligne lue à l'accumulateur *)
      | exception End_of_file -> List.rev acc (* Renvoie la liste accumulée quand on atteint la fin du fichier *)
  in
  loop []

Dans cette nouvelle syntaxe, l'exception est devenue un type de retour de la fonction qu'il est possible d'intégrer dans le filtrage par motif. De cette manière, il n'est plus nécessaire de mettre en place un bloc trywith et la fonction peut maintenant être optimisée par le compilateur. Cette nouvelle approche a, de plus, l'avantage d'être plus lisible et beaucoup plus concise.

Modifications du système de module

Les langages de la famille ML ont un riche système de modules qui se distingue notamment par sa notion de foncteur.
Un module est une sorte de paquetage contenant des définitions de type, parfois totalement abstraites, et les fonctions agissant sur ces types qui vont avec.
Autrement appelés modules paramétrés, les foncteurs sont aux modules ce que les fonctions sont aux valeurs. Tout comme une fonction construit une nouvelle valeur à partir de valeurs paramètres, un module paramétré permet de construire un nouveau module à partir de modules déjà construits. On peut faire une analogie avec la généricité dans le monde objet.
Cependant, cette richesse peut entraîner des comportements non-intuitifs pour l'utilisateur. Cette nouvelle version du compilateur corrige trois problèmes fréquemment rencontrés avec le système de modules.

Alias de module

Notamment, dans les versions antérieures d'OCaml, la pratique courante de définir un alias court pour un module

module S = Module_with_a_very_long_name

avait pour effet de créer une nouvelle copie du module Module_with_a_very_long_name. Cette copie inutile augmentait parfois fortement la taille des exécutables générés. Dans cette nouvelle mouture du compilateur, le module S est défini comme un simple alias évitant cette copie potentiellement coûteuse.

Foncteur génératif

Un autre exemple de comportement non-intuitif apparaissait lors de l'utilisation de foncteur pour générer des modules uniques. Imaginons par exemple que nous souhaitons générer plusieurs types uniques d'identifiants avec un foncteur de la forme :

module MakeId (M : sig end ) : sig type t ... end = struct 
  type t = int
  ...
end

On peut alors construire des nouveaux modules d'identifiants avec :

module Id1 = MakeId(struct end)
module Id2 = MakeId(struct end)

Les deux types Id1.t et Id2.t sont alors distincts et l'on ne peut pas mélanger les deux types d'identifiants. Cependant écrire à chaque fois un module vide est relativement pénible. Une idée serait d'écrire plutôt :

module E = struct end
module Id1 = MakeId(E)
module Id2 = MakeId(E)

Malheureusement, avec cette définition les deux modules générés sont identiques car générés à partir du même module E.

La nouvelle extension sur les foncteurs génératifs simplifie ce cas particulier où le foncteur prend le module "vide" en argument. Avec cette extension on peut écrire plus simplement :

module MakeId () : sig type t ... end = struct 
  type t = int
  ...
end

module Id1 = MakeId()
module Id2 = MakeId()

et obtenir deux types distincts Id1.t et Id2.t.

Ouverture locale de modules

Afin de faciliter l'utilisation des modules, OCaml, depuis la version 3.12, dispose de plusieurs constructions syntaxiques pour ouvrir localement un module, i.e. importer les identifiants du module dans l'espace de noms courant.

Par exemple, pour utiliser l'opérateur + pour additionner des float, il est possible
de définir un module :

module Float = struct
  let ( + ) = ( +. )
end

Ouvrir ce module globalement aurait l'inconvénient de masquer l'addition des entiers, mais on peut l'ouvrir localement

let x = 
   let open Float in
   1. + 5.

let y = Float.( 1. + 5. )

Dans la définition de y, le module Float est ouvert uniquement à l'intérieur des parenthèses. Cette construction s'est révélée très appréciée et vient d'être étendue pour éviter certains doublons de parenthèses

let list_f = Float.[ 1. + 2.; 3. ] (*  plutôt que Float.( [ 1. + 2.; 3. ] ) *)

let array_f = Float.[| 1. + 2. ; 3. |] (* plutôt que Float.( [| 1. + 2.; 3. |] ) *)

type record_t = { content : float}
let record = Float.{content = 1. + 2. }  (* plutôt que Float.({content = 1. + 2. }) *)

Types ouverts

Le système de type d'OCaml s'enrichit d'une nouvelle catégorie de type, les types ouverts qui sont à mi-chemin entre les types sommes classiques et les variants polymorphes.

Petit rappel sur les types enumérés existants

Types sommes

Dans les types sommes, l'ensemble des constructeurs est déterminé lors de la définition du type

type card = King | Queen | Jack | As | Suit of int

Comme le type est figé, il est facile de vérifier dans un filtrage par motif que tous les cas possibles sont gérés. Par contre une fois cette définition posée, les constructeurs du type card sont fixés et rien ne peut les changer. Si on souhaitait définir le type tarot_card à partir du type card, on serait limité à :

type tarot_card = Card of card | Knight

ce qui alourdit les notations. Par exemple, étendre la fonction

let is_jack card = match card with
  | Jack -> true
  | _ -> false

au type tarot_card nécessite de définir une nouvelle fonction

let is_jack_tarot tarot_card = match tarot_card with
  | Card card -> is_jack card
  | _ -> false

cette expression utilise l'underscore qui, en OCaml, signifie en quelque sorte le joker, ou en d'autres termes "pour tous les autres cas, fais ceci"

Variants polymorphes

Les variants polymorphes permettent de s'abstraire de cette limitation et d'utiliser des constructeurs directement sans avoir à les définir au préalable. Par exemple :

let seven = `Suit 7

définit une nouvelle variable à partir du constructeur `Suit. Contrairement aux apparences, le constructeur `Suit n'a rien à voir avec le constructeur Suit définit pour le type card. Le ` dénote le constructeur d'un variant polymorphe : n'importe quel nom commençant par une majuscule et précédé par ` est un constructeur de variant polymorphe classique. Pour l'instant le compilateur sait juste que le constructeur `Suit fait partie d'un ensemble ouvert qui n'est pas encore complètement défini.

Comme pour les types sommes, on peut utiliser le filtrage de motif pour définir des fonctions sur ces variants, par exemple :

let is_jack card = match card with
  | `Jack -> true
  | _ -> false

Avec cette définition, l'application is_jack seven est bien typée et retourne false comme attendu. Cependant, la fonction is_jack peut être appliquée à (presque) n'importe quel variant polymorphe : is_jack (`John `Do) est aussi bien typé et retourne false.

Le compilateur utilise un système de type relativement complexe pour vérifier que les expressions impliquant les variants polymorphes sont valides. Par exemple, appliquer la fonction :

let value_figure tarot_card = match tarot_card with
  | `King   -> 4.5
  | `Queen  -> 3.5
  | `Knight -> 2.5
  |` Jack   -> 1.5

à seven déclenchera une erreur de type durant la compilation car la fonction value_figure ne sait que faire du constructeur `Suit. Cependant, à cause de la flexibilité des variants polymorphes, certaines erreurs ne peuvent pas être détectées par le compilateur. Par exemple :

let is_suit card = match card with
  | `Suit -> true
  | _ ->  false

est une fonction OCaml valide, néanmoins is_suit seven déclenche une erreur de type parce que dans la fonction is_suit, le constructeur `Suit n'admet pas d'argument. La bonne version de cette fonction serait :

let is_suit card = match card with
  | `Suit _ -> true
  | _ ->  false

mais le compilateur n'a aucun moyen de le deviner.

L'apport des types ouverts

Les types ouverts sont, eux, des types sommes auquels on peut ajouter de nouveaux constructeurs après leur définition. Par exemple, on peut commencer par créer un type ouvert sans constructeur :

type open_card = ..

Puis, on peut rajouter les constructeurs de card dans un premier temps

type open_card += King | Queen | Jack | As | Suit of int

À partir de ce point, on peut réaliser une copie de ce type ouvert

type open_tarot_card = open_card = ..

et étendre cette copie

type open_tarot_card += Knight

La fonction

let is_jack card = match card with
  | Jack -> true
  | _ -> false

est alors de type open_card -> bool mais accepte en entrée tout type qui est une extension du type open_card: is_jack Knight est valide et renvoie false comme attendu.

Un point important dans la fonction précédente est la présence dans le filtrage de motif du motif _ : le type open_card étant ouvert, si on veut être exhaustif, il est nécessaire d'offrir une alternative pour les constructeurs qui ne sont pas encore connus. En cas d'oubli, le compilateur émet un avertissement pour signaler que le filtrage n'est pas exhaustif en présence d'extension.

Une différence majeure avec les variants polymorphes est que tous les constructeurs de open_card sont connus par le compilateur, qui peut donc plus facilement détecter certaines erreurs.

La fonction

let is_suit card = match card with
  | Suit -> true
  | _ -> false

déclenche donc bien une erreur de compilation à cause du mauvais usage du constructeur Suit contrairement au cas des variants polymorphes.

Un détail amusant est qu'il existait déjà une forme de type ouvert dans les versions antérieures d' OCaml : le type exn des exceptions qu'il était possible d'étendre en déclarant une nouvelle exception

exception Open

Avec l'apparition des types ouverts dans la version principale du compilateur, le type des exceptions est devenu un cas particulier de type ouvert.

Rappel sur les types algébriques généralisés

Introduits dans la version 4.0 il y a deux ans, les types algébriques généralisés (GADT) sont une évolution importante apportant plus d'expressivité au système de types.

Ce type de données permet de structurer un type somme en spécifiant quelles opérations sont autorisées sur chacune des valeurs possibles. Le but est bien sûr de donner plus de sécurité au programme en vérifiant dès la compilation qu'une action peut s'appliquer pour une valeur donnée.

Par exemple, un arbre bicolore oblige chaque nœud noir à avoir un parent rouge. Il est possible de vérifier cette règle en contrôlant à chaque opération d'insertion que l'ensemble de la cohérence de l'arbre est respectée. Toutefois, grâce aux GADT, ce contrôle peut être réalisé lors de la compilation du code : la cohérence de la structure de données sera ainsi garantie par le compilateur.

Cela permet de :

  • se reposer sur le compilateur pour garantir la structure de données : une erreur dans l'implémentation sera détectée au plus tôt.
  • accélérer la vitesse d'exécution du code : puisque la structure est garantie par le compilateur, il n'est pas nécessaire de contrôler chaque opération à l'exécution.

Avant les GADT

Imaginons que nous nous intéressons à des expressions mélangeant des entiers, des booléens et les opérations usuelles sur ces types ; par exemple, (7 + 8) = 15 && false.

Une manière de les représenter en OCaml avec un type somme est la suivante :

type expre =
  | Eq of expre * expre
  | Nbr of int 
  | Bool of bool 
  | Plus of expre * expre 
  | Minus of expre * expre 
  | Or of expre * expre 
  | And of expre * expre

Le problème de cette représentation est que l'expression

7 || (true + 4)

représentée dans ce type par

Or (Nbr 7, Plus (Bool true, Nbr 4))

est bien typée, car elle respecte la définition du type. Du point de vue du compilateur, tout va bien, même si sémantiquement, cette expression est absurde.

La résolution classique de cette ambigüité passe par une redéfinition un peu bavarde du type expre

type expre =
  | BoolExpre of bool_expre
  | IntExpre of int_expre

and bool_expre =
  | BoolValue of bool
  | BoolEq of bool_expre * bool_expre
  | IntEq of int_expre * int_expre
  | BoolOr of bool_expre * bool_expre
  | BoolAnd of bool_expre * bool_expre

and int_expre =
  | IntValue of int
  | IntPlus of int_expre * int_expre
  | IntMinux of int_expre * int_expre

Dans la pratique, on rencontre très fréquemment ce problème de typage dans la définition des types dérivés d'un type de base, par exemple un type “paramètre d'un programme” dont dérivent des types “callback lors de la mise à jour” ou “fonction de sérialisation du paramètre”: l'ajout d'un cas dans le type de base demande l'ajout d'un ou plusieurs cas dans les types dérivés.

Avec les GADT

Les GADT permettent de redéfinir les expressions précédentes de sorte que les expressions correctement typées soient également sémantiquement valides. Les règles de validités sémantique, autrement dit l'"invariant", que l'on souhaite garantir, est donc exposé au compilateur, qui garantit que cet invariant est respecté. Redéfinissons notre type avec les GADT :

type _ expre =
  | Eq : ('a expre * 'a expre ) -> bool expre
  | Nbr : int -> int expre
  | Bool : bool -> bool expre
  | Plus : (int expre * int expre)  -> int expre
  | Minus : (int expre * int expre) -> int expre
  | Or : (bool expre * bool expre)  -> bool expre
  | And : (bool expre * bool expre) -> bool expre

Si on compare cette définition à celle d'un type somme classique, une première différence saute aux yeux : les constructeurs (Eq, Nbr, …) sont explicitement typés. Typer explicitement les constructeurs permet d'exprimer les relations de dépendances entre les constructeurs, le type de leurs arguments et le type résultant. Par exemple, le constructeur Eq : 'a expre * 'a expre -> bool expre construit une expression booléenne à partir de deux expressions de même type : la sémantique de l'égalité est encodée directement dans le système de type. Le compilateur peut donc rejetter l'expression sémantiquement invalide

# Or (Nbr 7, Plus (Bool true, Nbr 4));;
Error: (Nbr 7) has type int expre
       but an expression was expected of type bool expre
       Type int is not compatible with type bool

On peut aussi utiliser ce type algébrique généralisé pour définir une fonction d'évaluation de ces expressions

let rec eval : type etype. etype expre -> etype = function
  | Nbr n  ->  n
  | Bool b ->  b 
  | Eq (left, right) -> (eval left) = (eval right)
  | Plus (left, right) -> (eval left) + (eval right)
  | Minus (left, right) -> (eval left) - (eval right)
  | Or (left, right) -> (eval left) || (eval right)
  | And (left, right) -> (eval left) && (eval right)

Plusieurs choses intéressantes se voient dans cette fonction d'évaluation. Premièrement, le type de la fonction est défini explicitement, avec notamment la déclaration du type etype, que l'on "invente" pour l'occasion. Deuxièmement, le type de retour correspond au paramètre de type dans l'expression : il s'agit d'une fonction polymorphe, car la fonction renvoi le type "inventé". Mais si on regarde le type etype à l'intérieur du filtrage du motif, on se rend compte que ce type varie en fonction de la branche dans laquelle on se trouve. À l'intérieur de la branche Nbr n , etype est un entier etype=int tandis que dans la branche Eq (left, right), expr est de type etype=bool alors que left et right sont de type 'a expre. Les constructeurs des GADT imposent donc des contraintes de types différentes pour chaque branche. Cela permet d'exprimer des fonctions qui ont un comportement et un type différent pour chacun des constructeurs du GADT.

On voit dans cet exemple que grâce aux GADT, la définition d'une fonction d'évaluation ne requiert pas l'introduction d'un type nouveau : on peut décrire la fonction d'évaluation et donc en particulier son type directement à partir du type de base. Avec l'ancienne définition du type expre, nous aurions du introduire un nouveau type somme décrivant le résultat d'une évaluation, et de surcroît, nous n'aurions pas pu représenter le fait que la fonction d'évaluation transforme une expression booléenne (resp. numérique) en valeur booléenne (resp. numérique) dans le système de types.

Changements dans les bibliothèques standard

Chaînes de caractères immutables

En OCaml les chaînes de caractères de type string ont été historiquement conçues comme mutables. Cependant, au fil des années, le consensus a évolué pour considérer qu'il était généralement préférable d'utiliser ce type comme s'il était immutable. Cette différence entre la pratique et le système de type était une bizarrerie pour un langage fonctionnel qui se targue de l'expressivité de son système de types. Il a donc été décidé de séparer le type string et le module attenant String en deux : d'un côté les chaînes de caractères immutables String, d'un autre les tampons mutables Bytes. Néanmoins, une séparation brutale aurait cassé un trop grand nombre de bibliothèques tierces. Pour adoucir cette séparation, le compilateur considère par défaut que les modules Bytes et String et les types associés sont identiques. Pour activer la distinction entre ces modules, il est nécessaire de passer l'option -safe-string au compilateur durant la compilation.

Usages des GADT dans la bibliothèque standard

Les types algébriques généralisés étant un ajout récent dans le compilateur, ils ne sont pas encore très utilisés dans la bibliothèque standard d'OCaml. Cependant, dans cette nouvelle version, les GADT apparaissent dans deux composants importants d'OCaml : le module Printf et la bibliothèque Bigarray.

La bibliothèque Bigarray

La distribution standard du compilateur OCaml inclut une bibliothèque de manipulation des tableaux multidimensionnels nommée Bigarray. Conçue pour s'interfacer facilement avec le C et le Fortran, elle est basée sur un type relativement complexe : ('a, 'b, 'c) Genarray.t. Les trois paramètres de type correspondent à :

  • 'a : le type de la représentation OCaml des éléments
  • 'b : le type de la représentation en C des éléments
  • 'c : l'ordre des dimensions dans le tableau (i.e. layout fortran ou C)

Laissons de côté pour l'instant le type 'c. Si on regarde les types 'a et 'b, il est clair que les deux sont interdépendants. Pour gérer cette interdépendance, la bibliothèque Bigarray introduisait un type abstrait ('a, 'b) kind qui représentait un couple valide de représentation OCaml et C. Les valeurs possibles de ce type étaient réalisées grâce à des variables opaques telles que :

  • float32 : (float, float32_elt) kind
  • int : (int, int_elt) kind

Le problème avec ces variables opaques était qu'il est totalement impossible de les manipuler, ce qui posait problème dans certaines situations.

Pour pallier ce problème, le type abstrait kind a été remplacé par un GADT de la forme :

type (_,_) kind = 
  | Float32 : (float, float32_elt) kind 
  | Int : (int, int_elt) kind
  | ...

L'avantage de cette construction est qu'elle permet d'exprimer les contraintes sur les types 'a et 'b directement dans la définition du type kind. De ce fait, il n'est plus nécessaire d'utiliser des variables opaques pour faire respecter l'interdépendance entre 'a et 'b. En utilisant les variables de type kind, il devient possible de définir une fonction renvoyant l'équivalent du zéro pour le type OCaml 'a

let zero (type a) (type b) ( k : (a,b) kind) : a = match k with
  | Float32 -> 0. (* float *)
  | Int -> 0      (* int *)
  | ...

L'utilisation des types algébriques généralisés permet donc d'avoir un comportement et un type spécifique pour chacune des branches du filtrage de motif. À partir de cette fonction zero, on peut ensuite écrire une fonction générique qui construit un tableau rempli de zéros

let create_zero kind layout dims =
  let array = Genarray.create kind layout dims in (* uninitialized array *)
  Genarray.fill (zero kind);
  array

Une autre utilisation possible serait de calculer la taille en bytes d'un tableau sans avoir à passer par les détails de l'implémentation.

Réécriture de la bibliothèque Printf

Qu'est-ce que le module Printf ? Il s'agit tout simplement du module associé à la fonction C éponyme, mais à la sauce OCaml : avec l'inférence de type. Cela signifie que l'instruction suivante ne compilera pas :

# Printf.printf "Hello %d" "world";;
Error: This expression has type string but an expression was expected of type int

En effet, le paramètre world ne correspond pas au type %d attendu. Comment est-ce possible ? Jusqu'à présent grâce à une magie interne assez difficile à maintenir. Celle-ci a désormais été réécrite à l'aide de la syntaxe des GADT. Cela ne change pas grand-chose pour l'utilisateur (l'interface du module n'a pas changé), mais il s'agit d'une réécriture d'un module particulièrement compliqué, et un exemple de ce qu'il est possible de faire à travers les GADT.

Que devient OCaml aujourd'hui ?

La dernière dépêche remonte à maintenant deux ans, et était sortie pendant une transformation du langage : ces dernières années, OCaml est passé d'un langage universitaire, destiné à implémenter des algorithmes et enseigner l'informatique, à celui d'un langage professionnel qui se veut apte à répondre à des besoins actuels. Qu'en est-il aujourd'hui ?

Nouveau wiki

Un wiki est maintenant disponible à l'adresse suivante : http://ocaml.org/, le site se veut être la vitrine communautaire du langage.

Opam

Opam est le nouveau gestionnaire de paquets pour OCaml. À la manière de maven (pour java), ou pip (en python), il permet d'accéder à l'ensemble des bibliothèques disponibles à travers un seul outil. Il y a supplanté Godi qui n'avait pas la même visibilité. Signe des temps, opam a été développé par ocamlpro.

Opam permet de maintenir plusieurs versions du langage sur un même poste, et de basculer d'une version à une autre. Créer un dépôt local est aussi relativement aisé et cela plus encore dans la future version 1.2 (actuellement en beta) qui permet d'installer un nouveau paquet à partir de n'importe quel dépôt git contenant les métadonnées requises.

Accès au code via github

La communauté s'est posée la question de savoir comment obtenir des retours de la part des contributeurs. La liste de diffusion est active, mais il s'agit d'un principe vieillissant, qui ne facilite pas les échanges de code. Le serveur hébergeant le code est le serveur de l'INRIA basé sur un svn.

Il a été décidé expérimentalement d'ouvrir OCaml sur github pour voir si les retours seraient plus nombreux en terme de contributions (voir la discussion sur la liste de diffusion et l'annonce sur le blog de l'équipe Gallium).

L'ensemble des branches de développement sont disponibles à travers opam, ce qui permet de basculer très facilement sur une version expérimentale pour tester les fonctionnalités à venir.

Nombreuses bibliothèques pour la vraie vie

Lorsqu'on regarde l'ensemble des bibliothèques disponibles pour OCaml, nombre d'entre elles servent à des problématiques vraiment pratiques, ne couvrant pas forcément que des sujets théoriques ; citons par exemple :

Pour le réseau

  • Ocsigen, un framework pour écrire très rapidement des applications web (client et serveur) entièrement en OCaml
  • Js_of_ocaml est un compilateur développé par le projet ocsigen traduisant le bytecode OCaml en code javascript
  • Ocamlnet, la bibliothèque couteau suisse pour de nombreux protocoles réseau. Cette bibliothèque énorme a été utilisée en production chez MyLife.com. Elle compte, entre autres, nethttpd, un serveur web haute performance, très scalable.
  • Un binding SSL
  • Une implémentation de TLS
  • Une bibliothèque pour le SMTP
  • Un serveur WebSocket

Bibliothèques côté clients

Format de fichiers

Autres projets intéressants

On pourra trouver une liste assez exhaustive des bibliothèques disponibles sur caml.org

Livres

Quelques livres sont sortis depuis l'année dernière sur le langage. Ils n'ont pas été présentés sur le site au moment de leur parution, voici donc l'occasion de les présenter pour ceux qui veulent s'intéresser au langage :

Real World OCaml, sorti en novembre 2013, présente le langage, mais se veut être un livre pratique pour répondre aux cas d'utilisation que l'on peut rencontrer au quotidien : comment écrire un parseur syntaxique, la construction d'une interface graphique à travers les mixins… Il a été écrit en partenariat avec les utilisateurs OCaml puisque tout le monde pouvait contribuer à sa rédaction, et est aujourd'hui entièrement disponible en ligne.

OCaml from the Very Beginning permet de se plonger dans le langage en douceur. Il est destiné à un public ne connaissant pas le langage et souhaitant le découvrir.

More OCaml : Algorithms, Methods & Diversions, tout récemment, complète le précédent et permet d'aller plus loin dans le langage.

Remerciements

L'écriture de cette dépêche n'aurait pas été possible sans la participation de tous. Merci à BAud, chimrod, Def, Jacques-Pascal Deplaix, jumbojet, Michaël, octachron, Ontologia, palm123, Snark et Tonton Th pour leur remarques et le temps passé à l'écriture.

Aller plus loin

  • # Tant de changements pour une version mineure

    Posté par  . Évalué à 8.

    Ce changement mineur de version n'est pas représentatif des évolutions présentes. Cette version inclut des changements dans la syntaxe du langage et des outils, pouvant casser la compatibilité avec les programmes existants.

    Il change de syntaxe entre les versions mineures ? Et ben, que cela doit être pour les versions majeures :).

    • [^] # Re: Tant de changements pour une version mineure

      Posté par  (site web personnel) . Évalué à 8.

      Le changement de syntaxe de cette version peut certes casser la compatibilité avec les programmes existants, mais sur des corners case tellement particuliers, que ça doit être assez rare.

      Quand aux changements dans les versions majeurs, on est dans le même cas. Il faut aller vraiment loin dans le langage pour tomber sur un cas où un problème va surgir.

      Le noyau de OCaml, que la plupart des gens utilisent (dont moi), est totalement stable depuis 1996.

      Pas de quoi s'alarmer, donc…

      « Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker

      • [^] # Re: Tant de changements pour une version mineure

        Posté par  (site web personnel) . Évalué à 10.

        Pour ma part, je trouve que les changements induits (librairies sorties du projet principal) justifieraient un changement de version majeure. Un programme écrit pour OCaml 4.01 ne compilera pas directement avec OCaml 4.02, sans changements dans le projet ; non pas à cause de changements de syntaxe, qui, comme tu le fais remarquer est très stable, mais à cause de l'environnement qui évolue (dépendance vers camlp4 qui ne sera plus disponible par défaut).

        En même temps, j'ai l'impression que OCaml 4.XX marque une rupture dans le langage. Historiquement, un bon langage était un langage avec une bonne syntaxe, qui évitait au développeur de passer du temps sur des choses simples. Aujourd'hui ça n'est plus suffisant : un bon langage doit « être fourni avec des piles » pour reprendre le slogan de python.

        Pendant longtemps, c'est ce qui a manqué à OCaml. Deux bibliothèques concurrentes ont essayées de combler ce manque, batteries et core, et il semble qu'aujourd'hui core soit en train de gagner la bataille. ( + Opam vient également changer la donne dans la diffusion des bibliothèques ).

        Pour moi, cela représente davantage que les changements de syntaxes. Un langage ne peut pas être résumé à sa grammaire s'il veut se diffuser. OCaml à la chance d'avoir une bonne syntaxe, élégante et très puissante. S'il n'a pas percé jusqu'alors, c'est pour d'autres raisons, et je pense que cette version 4.XX est en train de marquer ces changements.

        C'est bien beau de pouvoir coder des arbres balancés, mais au quotidien, on a plus souvent besoin d'un parseur XML que d'un splay tree ! Je pense que c'est sur ce point que l'on va continuer à voir le langage évoluer et se diffuser.

        • [^] # Re: Tant de changements pour une version mineure

          Posté par  (site web personnel) . Évalué à 4.

          Opam vient également changer la donne dans la diffusion des bibliothèques

          Cela fait des années que je dis qu'il faut un CPAN pour OCaml… Ceci dis, si j'ai bien compris, ce n'est pas encore un CPAN car cela reste un annuaire qui télécharge vers le repository alors qu'un CPAN (ou CTAN) contient une copie locale de la version du paquetage. Pour l'historique et la pérennité, c'est très différent. Il n'y a par exemple aucune dépendance sur sourceforge ou github par exemple…

        • [^] # Re: Tant de changements pour une version mineure

          Posté par  . Évalué à 4.

          Il y a plusieurs choses qui freinent encore l'expansion d'OCaml: cette absence de vraie bibliothèque standard (aucune des deux n'est livrée avec par défaut, et aucun choix courageux n'est fait entre l'une et l'autre) ; et une histoire d'œuf et de poule: peu de bibliothèques (si on compare à Python ou Java par exemple).

          Si une décision est enfin prise sur la bibliothèque standard, on ne peut que s'en réjouir.

          • [^] # Re: Tant de changements pour une version mineure

            Posté par  . Évalué à 3.

            Le manque de bibliothèque est loin d’être aussi terrible que les gens ici l'affirment. Il y a des domaines sous représentés (la 3D, le calcul numérique), mais tu as déjà énormément de chose, et souvent d'une très bonne qualité.

            Tu as un besoin précis qui n'est pas couvert, ou alors c'est une affirmation générale "la communauté est plus petite, il manque forcement des bibliothèques" ?

            • [^] # Re: Tant de changements pour une version mineure

              Posté par  (site web personnel) . Évalué à 1. Dernière modification le 07 septembre 2014 à 21:50.

              D'abord, ce n'est qu'un annuaire et non un vrai CPAN mais il manque aussi je pense la gestion des espaces de nom comme sur le CPAN qui en fait d'ailleurs sa grande force. En effet, avec le système collaboratif du CPAN, avec le temps, certain espaces de nom se sont enrichis et c'est la communauté qui régule cela toute seule.

              On a avec OCaml certes une liste de paquetage mais à mon sens en vrac et dont les noms sont pas forcément parlant ni hiérarchisé… On a d'ailleurs à ce niveau là le même soucis en Python ou chaque auteur choisis le nom qu'il veut mais pas celui du voisin. L'émergence et la vie des branches dans l'espace de nom du CPAN est vraiment une merveille…

              • [^] # Re: Tant de changements pour une version mineure

                Posté par  (site web personnel) . Évalué à 2.

                On me précise dans mon oreillette que OPAM a des tags, et mon oreillette me précise "c'est mieux qu'une hierarchie qui est toujours fondamentalement cassée".

                Reste à faire le site en fonction…

                « Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker

                • [^] # Re: Tant de changements pour une version mineure

                  Posté par  (site web personnel) . Évalué à 3.

                  Ca n'a rien à voir, les paquetages Debian ont une catégorie et des tags qui permettent de faire une recherche par facette…

                  Les espaces de nom permettent de regrouper sous une même branche des modules, des branches se développent, d'autres meurent… Si tu ne fais pas cela, tu te retrouves avec 100000 fichiers en vrac à la racine de ton système de fichier ;-) Sur le CPAN, tu peux regarder par exemple du coté de DBIx ou de MouseX… Il y a un coté réseau social ou des personnes vont venir déposer leur module dans une hiérarchie afin de lui apporter de nouvelles fonctionnalités.

                  Je ne pense pas que les TAG aient le même rôle et je suis sur que l'aspect collaboratif fonctionne moins bien pour construire un ensemble cohérent de module.

                  Dernier point, les sources ne sont pas dans OPAM à ce que j'ai vu, c'est un ensemble de lien vers des sources externes. Tout peux donc être cassé à tout moment. Il faut à mon sens intégrer tout le code source (libre) dans des serveurs centraux gérés par une fondation.

          • [^] # Re: Tant de changements pour une version mineure

            Posté par  . Évalué à 2.

            Il y a plusieurs choses qui freinent encore l'expansion d'OCaml: cette absence de vraie bibliothèque standard

            Si je ne dis pas de bêtise, il me semble que c'est la même histoire que le langage D : pas de librairie standard (je sais on dit bibliothèque mais j'ai horreur de ce terme) d’où 2 librairies principales d’où soucis pour créer des librairies tierces.

            Inversement avec Python ou Go qui permet une progression rapide de écosystème. Non ?

            • [^] # Re: Tant de changements pour une version mineure

              Posté par  . Évalué à 2.

              Hum, avec Python3 ça n'est plus vrai..

              • [^] # Re: Tant de changements pour une version mineure

                Posté par  (site web personnel) . Évalué à 2.

                De toute manière, c'est très difficile de savoir pourquoi tel projet a marché a telle époque et tel autre plus tard… Il y a quelques règles de bonnes conduites je pense mais il y a aussi un effet buzz/mode peu prévisible qui fait qu'après, on voit des moments clefs mais que sur le coup, cela me semble rarement objectif.

            • [^] # Re: Tant de changements pour une version mineure

              Posté par  . Évalué à 2.

              Le livre Real World OCaml, qui utilise Core exclusivement, et qui est accessible gratuitement en ligne, ne va-t-il pas marginalisé Batteries Included ?

              J'ai l'impression que sans véritable concertation, mais plus par une plus grande activité de Core, cette lib standard est en train de s'imposer.

          • [^] # Re: Tant de changements pour une version mineure

            Posté par  (site web personnel) . Évalué à 4.

            Pendant longtemps j'ai pensé comme toi, et un jour j'ai posté mon tourment sur la ML officielle, un Allemand m'a répondu :
            "Ahhh, le centralisme à la française"

            J'ai reconsidéré ma position…
            Pour ma part, j'utilise batteries, et en entête de mes fichier, j'ai

            module L = struct
                    include BatList
                    (** Fonction supplémentaires pour les listes*)
                    (*let rec union m = function 
                            | []        -> m
                            | t :: q        -> t :: (union (L.remove  m t) q);;  *)
                    let rec union l1 l2 = let lr = append l1 l2 in unique lr
            
                    let rec intersect m = function
                            | []                     -> []
                            | t::q when mem t m -> t :: (intersect (remove m t) q) 
                            | t :: q                 -> intersect m q;; 
            
                    let rec difference  m1 m2 = match m1 with
                    | []                ->[]
                    | t::q when     mem t m2 -> difference q (remove m2 t)
                    | t::q              -> t::(difference  q m2);;
            
                    let included master sub =
                     let testInclude = L.difference sub master in
                     L.length testInclude = 0
            
            end;;
            
            
            module A = BatArray;;
            module S = BatString;;
            module O = BatOption;;

            « Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker

  • # Très bonne dépêche !

    Posté par  (site web personnel) . Évalué à 10.

    Très bonne dépêche ! j'ai beau suivre la caml-list, je reste un utilisateur me contentant du noyau historique. Cette introduction aux nouvelles fonctionnalités est vraiment bien faite et pédagogique. Merci !

    • [^] # Re: Très bonne dépêche !

      Posté par  . Évalué à 4.

      En effet, une excellente dépêche; ça fait plaisir.

      • [^] # Re: Très bonne dépêche !

        Posté par  . Évalué à 1.

        Héhé : Gasche (avec un G en plus) est la seule personne nommée de l'article. Du sponsoring sur le site ?

        Plus sérieusement, serait-il possible de développer la partie "Alias de module" avec un exemple d'utilisation montrant les nouvelles possibilités ainsi que les modifications possibles sur du code déjà existant ?

      • [^] # Re: Très bonne dépêche !

        Posté par  . Évalué à 2.

        Complètement hors-sujet, mais je tiens à te remercier pour ton travail quotidien sur ocaml et batteries. J'en profite tous les jours. Merci. :)

      • [^] # Re: Très bonne dépêche !

        Posté par  . Évalué à 2.

        J'ai decouvert avec plaisir que tu contribuais a OCaml. Merci pour tes contributions !
        Du coup je comprend beaucoup mieux ta connaissance theorique des langages de programmation ;)

    • [^] # Re: Très bonne dépêche !

      Posté par  . Évalué à 4.

      J'ai trouvé la description des GADTs particulièrement claire.

      J'avais déjà lu un peu dessus, sans en avoir une vision complètement juste, mais là, c'était limpide ! Merci !

    • [^] # Re: Très bonne dépêche !

      Posté par  . Évalué à 2.

      En effet, excellente depeche !
      Une telle densite est surprenante pour le passage d'une version 4.00 a une version 4.02.
      C'est egalement un beau travail de vulgarisation scientifique :)

      Merci de m'avoir fait decouvrir pfff ("Un ensemble d'outils pour réaliser de l'analyse de code dans de nombreux langages" dans la depeche). Le nom est quand meme pas terrible, mais ca a l'air surpuissant. Je suis content de voir que l'auteur de Coccinelle a rebondi sur l'idee et est alle beaucoup plus loin. Il fut un temps, j'avais regarde si quelque chose comme Coccinelle existait pour Java, mais ce projet m'etait passe sous le nez. Sympa de le retrouver :)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.